package com.tobeface.wechat.message;

import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.tobeface.modules.lang.Each;
import com.tobeface.modules.lang.Ghost;
import com.tobeface.modules.lang.Langs;
import com.tobeface.modules.lang.Requires;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
 * @author loudyn
 */
final class WeChatMessagePayloadHelper {

    private static final String WECHAT_MESSAGE_XML_ROOT = "xml";
    private static final String ITERABLE_ELEMENT = "item";
    private static final DocumentBuilderFactory DOM_FACTORY = DocumentBuilderFactory.newInstance();
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();

    /**
     * @param xml
     * @return
     */
    static Map<String, Object> loads(String xml) {
        Requires.notNull(xml);
        return loads(xml.getBytes(Charset.forName("UTF-8")));
    }

    /**
     * @param xml
     * @return
     */
    static Map<String, Object> loads(byte[] content) {

        if (content.length == 0) {
            return Collections.emptyMap();
        }

        Document doc = createDocument(content);
        NodeList childs = doc.getElementsByTagName(WECHAT_MESSAGE_XML_ROOT).item(0).getChildNodes();

        Map<String, Object> payloads = Maps.newHashMap();
        for (int i = 0; i < childs.getLength(); i++) {
            Node node = childs.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            payloads.put(node.getNodeName(), node.getTextContent());
        }
        return payloads;
    }

    /**
     * @param payloads
     * @return
     */
    static String dumps(Map<String, Object> payloads) {
        Requires.notNull(payloads);
        Document root = createEmptyDocument();
        Element xmlroot = root.createElement(WECHAT_MESSAGE_XML_ROOT);
        root.appendChild(xmlroot);

        Set<String> cdataElenames = Sets.newHashSet();
        dumps(root, xmlroot, payloads, cdataElenames);
        return dumpsXml(root, cdataElenames);
    }


    private static void dumps(Document root, Element parent,
                              Map<String, Object> value,
                              Set<String> cdataElenames) {

        for (Map.Entry<String, Object> entry : value.entrySet()) {

            Element child = root.createElement(entry.getKey());
            parent.appendChild(child);

            Ghost<?> ghost = Ghost.me(entry.getValue());
            if (ghost.openEyes().isMap()) {
                dumps(root, child, (Map<String, Object>) entry.getValue(), cdataElenames);
                continue;
            }

            if (ghost.openEyes().isIterable()) {
                dumps(root, child, entry.getKey(), (Iterable) entry.getValue(), cdataElenames);
                continue;
            }

            cdataElenames.add(entry.getKey());
            child.setTextContent(entry.getValue().toString());
        }

    }

    private static void dumps(final Document root, final Element parent,
                              final String elename, Iterable<?> value,
                              final Set<String> cdataElenames) {
        Langs.each(value, new Each<Object>() {
            @Override
            public void invoke(int index, Object which) {

                Element child = root.createElement(ITERABLE_ELEMENT);
                parent.appendChild(child);

                Ghost<?> ghost = Ghost.me(which);
                if (ghost.openEyes().isMap()) {
                    dumps(root, child, (Map<String, Object>) which, cdataElenames);
                    return;
                }

                child.setTextContent(which.toString());
                cdataElenames.add(elename);
            }
        });

    }

    private static String dumpsXml(Document root, Set<String> cdataElenames) {
        try {

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            DOMSource domSource = new DOMSource(root);
            StreamResult result = new StreamResult(out);
            Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

            String cdataElements = Joiner.on(" ").join(cdataElenames);
            transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, cdataElements);
            transformer.transform(domSource, result);
            return new String(out.toByteArray(), Charset.forName("UTF-8"));
        } catch (Exception e) {
            throw Langs.toUncheck(e);
        }
    }

    private static Document createDocument(byte[] xml) {
        try {

            DocumentBuilder builder = DOM_FACTORY.newDocumentBuilder();
            InputStream in = new ByteArrayInputStream(xml);
            return builder.parse(in);
        } catch (Exception e) {
            throw Langs.toUncheck(e);
        }
    }

    private static Document createEmptyDocument() {
        try {

            return DOM_FACTORY.newDocumentBuilder().newDocument();
        } catch (Exception e) {
            throw Langs.toUncheck(e);
        }
    }

    private WeChatMessagePayloadHelper() {
    }
}
